package com.dynamic.mybatis.core.parser.xml;

import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.dynamic.mybatis.core.enums.CommandType;
import com.dynamic.mybatis.core.enums.ReturnType;
import com.dynamic.mybatis.core.exceptions.DmsDocumentException;
import com.dynamic.mybatis.core.generator.MapperMethod;
import com.dynamic.mybatis.core.metadata.DynamicConfiguration;
import com.dynamic.mybatis.core.metadata.DynamicMappedStatement;
import com.dynamic.mybatis.core.parser.NamespaceHelper;
import com.dynamic.mybatis.core.parser.field.DynamicMixedMappedStatementFieldParser;
import com.dynamic.mybatis.core.parser.xml.expand.MyDefaultElement;
import com.dynamic.mybatis.core.parser.xml.expand.MySAXReader;
import com.dynamic.mybatis.core.parser.xml.validator.DmsXMLMapperEntityResolver;
import com.dynamic.mybatis.core.parser.xml.validator.MybatisXmlValidator;
import com.dynamic.mybatis.core.session.DynamicSqlSession;
import com.dynamic.mybatis.core.utils.BeanUtils;
import com.dynamic.mybatis.core.utils.CollectionUtils;
import com.dynamic.mybatis.core.utils.LambdaUtils;
import org.apache.ibatis.builder.xml.XMLMapperBuilder;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ResultMap;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.parsing.XNode;
import org.apache.ibatis.parsing.XPathParser;
import org.apache.ibatis.session.Configuration;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.xml.sax.SAXParseException;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * @Description
 * @Author xs
 * @Date 2021/11/10 15:48
 */
public abstract class DynamicBaseMappedStatementParser {
    private static final Log log = LogFactory.getLog(DynamicBaseMappedStatementParser.class);
    protected static final String trimBlankRegex = "^\\s*|\\s*$";
    protected static final String[] statementXmlTags = "<select|<insert|<update|<delete|<parameterMap|<resultMap|<sql".split("\\|");
    protected static final String[] evalTags = Stream.of("update", "insert", "select", "delete", "reference").toArray(String[]::new);
    protected Configuration configuration;
    protected DynamicSqlSession dynamicSqlSession;

    public DynamicBaseMappedStatementParser(DynamicSqlSession dynamicSqlSession) {
        this.dynamicSqlSession = dynamicSqlSession;
        this.configuration = dynamicSqlSession.getSqlSession().getConfiguration();
    }

    public static boolean isXml(String str) {
        for (String tag : statementXmlTags) {
            if (str.startsWith(tag)) {
                return true;
            }
        }
        return false;
    }

    public void parseMapperXmlBodyAddConfiguration(String mapperBodyXml, String namespace) {
        parseMapperXmlAddConfiguration(MybatisXmlValidator.buildXmlMapperBody(mapperBodyXml, namespace));
    }

    public void parseMapperXmlAddConfiguration(String xml) {
        DynamicConfiguration dynamicConfiguration = (DynamicConfiguration) configuration;
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xml.getBytes());
        Map<String, XNode> sqlFragments = new HashMap<>();
        XMLMapperBuilder xmlMapperBuilder = createXMLMapperBuilder(byteArrayInputStream, configuration, dynamicConfiguration.DYNAMIC_RESOURCE, sqlFragments);
        xmlMapperBuilder.parse();
        dynamicConfiguration.addSqlFragments(sqlFragments);
    }



    public Set<String> setDynamicMappedStatementTables(DynamicMappedStatement dms,Set<String> tables) {
       if(CollectionUtils.isEmpty(tables)){
            return Collections.emptySet();
        }
        String sqlStatement = dms.getSqlStatement();
        tables =  tables.stream().filter(table ->
                Pattern.compile("\\b"+ Pattern.quote(table) + "\\b").matcher(sqlStatement).find()
                        && !Pattern.compile("<(.*?)"+Pattern.quote(table)+"(.*?)>").matcher(sqlStatement).find()
        ).collect(Collectors.toSet());
        dms.setTables(tables);
        return tables;
    }

 /*   public Set<String> setDynamicMappedStatementTables(DynamicMappedStatement dms, String namespace) {
        if (dms == null) {
            return Collections.emptySet();
        }
        Configuration configuration = dynamicSqlSession.getSqlSession().getConfiguration();
        //临时namespace
        String mappedStatementId = NamespaceHelper.getDynamicMappedStatementId(dms.getTenantCode(),namespace, dms.getShortId());
        MappedStatement mappedStatement = !Objects.equals(dms.getId(), mappedStatementId) ? configuration.getMappedStatement(mappedStatementId) : null;
        //外部加载真实namespace
        if (mappedStatement == null) {
            mappedStatement = configuration.getMappedStatement(dms.getId());
        }

        Object parameter = Mock.mock(dms.getInputParameter().get(0));
        if (dms.isBatch() && CollectionUtils.isNotEmpty(parameter)) {
            parameter = ((Collection) parameter).toArray()[0];
        } else {
            parameter = dms.getOutputParameterMock();
        }

        String sql = mappedStatement.getSqlSource().getBoundSql(parameter).getSql();
        String urlSplit[];
        try (Connection connection = dynamicSqlSession.getConnection(dms.getId());) {
            urlSplit = connection.getMetaData().getURL().split(":");
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
        List<SQLStatement> sqlStatements = SQLUtils.parseStatements(sql, urlSplit[1]);
        Collection<TableStat.Column> tableStatColumns = null;
        for (SQLStatement sqlStatement : sqlStatements) {
            SchemaStatVisitor visitor = SQLUtils.createSchemaStatVisitor(urlSplit[1]);
            sqlStatement.accept(visitor);
            tableStatColumns = visitor.getColumns();
        }
        Set<String> tables = tableStatColumns.stream().map(TableStat.Column::getTable).collect(Collectors.toSet());
        if (CollectionUtils.isNotEmpty(dms.getTables())) {
            tables.addAll(dms.getTables());
        }
        dms.setTables(tables);
        return tables;
    }*/

    protected List<ResultMap> parserResultMap(String id, String resultType, String resultMap) {
        List<ResultMap> resultMaps = new ArrayList<>();
        if (StringUtils.isNotBlank(resultType)) {
            Class resultTypeClass = configuration.getTypeAliasRegistry().resolveAlias(resultType);
            ResultMap resultMapObj = new ResultMap.Builder(configuration, id + "-Inline", resultTypeClass, new ArrayList<>(), null).build();
            resultMaps.add(resultMapObj);
        } else if (StringUtils.isNotBlank(resultMap)) {
            XNode xNode = new XPathParser(resultMap).evalNode("/resultMap");
            id = xNode.getStringAttribute("id");
            DynamicMappedStatement dms = new DynamicMappedStatement();
            dms.setShortId(id);
            dms.setSqlStatement(resultMap);
            parseDynamicMappedStatement(dms, true);
            resultMaps.add(configuration.getResultMap(id));
        }
        return resultMaps;
    }

    public DynamicMappedStatement parseDynamicMappedStatement(DynamicMappedStatement dms, boolean handler) {
        try {
            //解析动态新xml SQL
            if (isXml(trimBlank(dms.getSqlStatement()))) {
                DynamicConfiguration dynamicConfiguration = (DynamicConfiguration) configuration;
                String xml = MybatisXmlValidator.buildXmlMapperBody(dms.getSqlStatement(), NamespaceHelper.getNamespace(dms.getTenantCode(),dms.getNamespace()));
                ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xml.getBytes());
                Map<String, XNode> sqlFragments = new HashMap<>();
                XMLMapperBuilder xmlMapperBuilder = createXMLMapperBuilder(byteArrayInputStream, configuration, dynamicConfiguration.DYNAMIC_RESOURCE, sqlFragments);
                xmlMapperBuilder.parse();
                dynamicConfiguration.addSqlFragments(sqlFragments);

                dynamicConfiguration.addDynamicMappedStatement(dms, handler);
            } else {
                //SQL解析
                SqlSource sqlSource = createSqlSource(configuration, trimBlank(dms.getSqlStatement()), null);
                //初始化mappedStatement
                MappedStatement.Builder builder = new MappedStatement.Builder(configuration, dms.getId(), sqlSource, CommandType.getSqlCommandType(dms.getCommandType()));
                builder.resultMaps(parserResultMap(dms.getId(), dms.getResultType(), dms.getResultMap()));
                MappedStatement mappedStatement = builder.build();
                //将mappedStatement 加入mybatis上下文中
                configuration.addMappedStatement(mappedStatement);

                ((DynamicConfiguration) configuration).addDynamicMappedStatement(dms, handler);
            }
            //解析字段
            DynamicMixedMappedStatementFieldParser fieldParser = dynamicSqlSession.getDynamicMappedStatementHelper().getMixedMappedStatementFieldParser();
            fieldParser.parseMappedStatementField(dms);

        } catch (Exception e) {
            log.error(e.getMessage() +". mappedStatement is [" + dms.getId() + "]. ");
            throw e;
        }
        DynamicConfiguration dc = ((DynamicConfiguration) configuration);
        if (dms.getCommandType() == null) {
            MappedStatement mappedStatement = dc.getMappedStatement(dms.getId());
            dms.toBuilder().setCommandType(mappedStatement.getSqlCommandType());
        }
        if (!dc.hasMappedStatement(dms.getId())) {
            dc.addDynamicMappedStatement(dms, handler);
        }
        return dms;
    }

    public XMLMapperBuilder createXMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
        try {
            XPathParser xPathParser = new XPathParser(inputStream, true, configuration.getVariables(), new DmsXMLMapperEntityResolver());
            Constructor constructor = XMLMapperBuilder.class.getDeclaredConstructor(XPathParser.class, Configuration.class, String.class, Map.class);
            constructor.setAccessible(true);
            return (XMLMapperBuilder) constructor.newInstance(xPathParser, configuration, resource, sqlFragments);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        //  return  new XMLMapperBuilder(inputStream,configuration, resource, sqlFragments);
    }

    protected List<XNode> parser(String script) {
        List<XNode> xNodes = new ArrayList<>();
        for (String tag : statementXmlTags) {
            XNode xNode = createXPathParser(script).evalNode(tag);
            xNodes.add(xNode);
        }
        return xNodes;
    }

    public String validateXmlMapperBody(String xmlMapperBody, String namespace){
        String xml;
        try {
            xml = MybatisXmlValidator.buildXmlMapperBody(xmlMapperBody, namespace);
            MybatisXmlValidator.validate(xml, 3);
        } catch (Exception e) {
            if (e instanceof DmsDocumentException) {
                throw new DmsDocumentException(e);
            } else if (e instanceof SAXParseException) {
                SAXParseException saxException = (SAXParseException) e;
                throw new DmsDocumentException(e, e.getMessage(), saxException.getLineNumber(), saxException.getColumnNumber());
            } else {
                throw new DmsDocumentException(e);
            }
        }
        return xml;
    }

    protected XPathParser createXPathParser(String xml) {
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xml.getBytes());
        return new XPathParser(byteArrayInputStream, true, configuration.getVariables(), new DmsXMLMapperEntityResolver());
    }

    public List<DynamicMappedStatement> document4jParser(String tenantCode,String namespace,String xml) {
        Document document = createDom4jParser(xml);
        Element mapperElement = document.getRootElement();
        List<Element> elements = evalNodes(mapperElement, evalTags);
        List<DynamicMappedStatement> dmsList = new ArrayList<>();
        DynamicConfiguration dynamicConfiguration = (DynamicConfiguration) configuration;
        for (Element element : elements) {
            String shortId = element.attributeValue("id");
            DynamicMappedStatement dms;
            if (element.getName().equals("reference")) {

                String referenceId = element.attributeValue("referenceId");
                DynamicMappedStatement referenceDms = dynamicConfiguration.getDynamicMappedStatement(referenceId);
                if (referenceDms == null) {
                    String message = " does not contain value for " + referenceId;
                    if (element instanceof MyDefaultElement) {
                        MyDefaultElement myDefaultElement = (MyDefaultElement) element;
                        throw new DmsDocumentException(message, myDefaultElement.getLineNumber(), myDefaultElement.getColumnNumber());
                    } else {
                        throw new IllegalArgumentException(message);
                    }
                }
                String dmsId;
                if (StringUtils.isBlank(shortId)) {
                    dms = referenceDms;
                    dmsId = NamespaceHelper.getDynamicMappedStatementId(dms.getTenantCode(),namespace, dms.getShortId());
                } else {
                    dms = BeanUtils.serializeCopy(referenceDms);
                    dms = dms.toBuilder().setShortId(shortId).setNamespace(namespace).setReferenceId(referenceDms.getId()).builder();
                    dynamicConfiguration.addDynamicMappedStatement(dms, true);
                    dmsId = dms.getId();
                }
                MappedStatement ms = dynamicConfiguration.getMappedStatement(dmsId);
                if (ms == null) {
                    //生成新的MappedStatement
                    Element ele = getElementById(dms.getSqlStatement(), referenceDms.getShortId());
                    ele.attribute("id").setValue(dms.getShortId());
                    String elementXml = ele.asXML();
                    this.parseMapperXmlBodyAddConfiguration(elementXml, namespace);
                }

            } else {
                ReturnType returnType = ReturnType.getReturnType(element.attributeValue(LambdaUtils.getFieldName(DynamicMappedStatement::getReturnType)));
                MapperMethod mapperMethod = MapperMethod.get(shortId);
                returnType = returnType == null && mapperMethod != null ? ReturnType.getReturnType(mapperMethod.getReturnType().name()) : returnType;
                String method = CommandType.StandardCommand.SELECT.name().equalsIgnoreCase(element.getName()) ? "GET" : "POST";
                DynamicMappedStatement.Builder dmsBuilder = new DynamicMappedStatement().toBuilder()
                        .setShortId(shortId)
                        .setTenantCode(tenantCode)
                        .setNamespace(namespace)
                        .setSqlStatement(element.asXML())
                        .setCommandType(element.getName())
                        .setParameterWrapper(element.attributeValue(LambdaUtils.getFieldName(DynamicMappedStatement::getParameterWrapper)))
                        .setResultWrapper(element.attributeValue(LambdaUtils.getFieldName(DynamicMappedStatement::getResultWrapper)))
                        .setName(element.attributeValue(LambdaUtils.getFieldName(DynamicMappedStatement::getName)))
                        .setBatch(Boolean.valueOf(element.attributeValue(LambdaUtils.getFieldName(DynamicMappedStatement::isBatch))))
                        .setBatchSize(Integer.valueOf(Objects.toString(element.attributeValue(LambdaUtils.getFieldName(DynamicMappedStatement::getBatchSize)), "0")))
                        .setResultType(element.attributeValue(LambdaUtils.getFieldName(DynamicMappedStatement::getResultType)))
                        .setResultMap(element.attributeValue(LambdaUtils.getFieldName(DynamicMappedStatement::getResultMap)))
                        .setReturnType(returnType);
                dmsBuilder.setMethod(method);
                dms = dmsBuilder.builder();
            }
            dmsList.add(dms);
            mapperElement.remove(element);
        }
        return dmsList;
    }

    public Element parserDom4jMapperBodyXml(String mapperBodyXml, String namespace) {
        String xml = MybatisXmlValidator.buildXmlMapperBody(mapperBodyXml, namespace);
        return createDom4jParser(xml).getRootElement();
    }

    protected Document createDom4jParser(String xml) {
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8));
        SAXReader saxReader = new MySAXReader();
        saxReader.setEntityResolver(new DmsXMLMapperEntityResolver());
        try {
            return saxReader.read(byteArrayInputStream);
        } catch (DocumentException e) {
            throw new DmsDocumentException(e);
        }
    }

    protected String evalIgnoreNodes(Element element) {
        return evalIgnoreNodes(element, evalTags);
    }

    protected String evalIgnoreNodes(Element element, String... ignoreTagNames) {
        List<Element> elements = evalNodes(element, ignoreTagNames);
        elements.stream().forEach(e -> element.remove(e));
        return Objects.toString(element.elements().stream().map(e -> ((Element) e).asXML()).collect(Collectors.joining(System.lineSeparator())), "");
    }

    protected List<Element> evalNodes(Element element, String... tagNames) {
        List<Element> elementList = element.elements();
        if (elementList == null) {
            return Collections.EMPTY_LIST;
        }
        List<String> tags = Arrays.stream(tagNames).collect(Collectors.toList());
        return elementList.stream().filter(e -> tags.contains(e.getName())).collect(Collectors.toList());
    }

    /**
     * @param configuration   mybatis上下文
     * @param mappedStatement 动态SQL
     * @param parameterType   参数类型
     * @Description: 初始化sql对象
     */
    protected SqlSource createSqlSource(Configuration configuration, String mappedStatement, String parameterType) {
        Class parameterTypeClass;
        if (com.dynamic.mybatis.core.utils.StringUtils.isBlank(parameterType)) {
            parameterTypeClass = HashMap.class;
        } else {
            parameterTypeClass = configuration.getTypeAliasRegistry().resolveAlias(parameterType);
        }
        return configuration.getDefaultScriptingLanguageInstance().createSqlSource(configuration, mappedStatement, parameterTypeClass);
    }

    public Element getElementById(String mapperBodyXml, String id) {
        Element element = parserDom4jMapperBodyXml(mapperBodyXml, "test");
        for (Element e : (List<Element>) element.elements()) {
            if (Objects.equals(e.attributeValue("id"), id)) {
                return e;
            }
        }
        return null;
    }

    protected String trimBlank(String str) {
        if (str == null) {
            return str;
        }
        Pattern pattern = Pattern.compile(trimBlankRegex);
        Matcher matcher = pattern.matcher(str);
        return matcher.replaceAll("");
    }

    public static void main(String[] args) {
        String sqlStatement = "<select id=\"selectListRole\" parameterType=\"map\" resultType=\"map\" returnType=\"collect\" name=\"查询列表\">\n" +
                " SELECT \n" +
                "  r.name AS rname , \n" +
                "  (select user_account from user u where u.id = ur.user_id limit 1) as username\n" +
                "  FROM role r ,user_role ur\n" +
                "  <where>\n" +
                "  <if test=\"id != null\">id=#{id}</if>\n" +
                "\tand r.id in (select role_id from role_resource rr where rr.role_id = r.id)\n" +
                "  </where>\n" +
                "  ORDER BY r.id DESC\n" +
                "</select>\n";

        String [] tables = ("data_source\n" +
                "dictionary\n" +
                "mapped_statement\n" +
                "name_space\n" +
                "resource\n" +
                "role\n" +
                "role_resource\n" +
                "test\n" +
                "user\n" +
                "user_expand\n" +
                "user_role").split("\\n");
        //
        Set<String> set = Arrays.stream(tables).filter(table ->
                Pattern.compile("\\b"+ Pattern.quote(table) + "\\b").matcher(sqlStatement).find()
                && !Pattern.compile("<(.*?)"+Pattern.quote(table)+"(.*?)>").matcher(sqlStatement).find()
        ).collect(Collectors.toSet());
       System.out.println(JSON.toJSONString(set));
    }
}